package com.example.minisqlite;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 极简单文件存储：按 tableId + rowId 保存行数据，支持内存索引和顺序追加。
 */
public class StorageEngine implements Closeable {

    public static final int PAGE_SIZE = 4096;
    private static final int HEADER_SIZE = 128;

    private static final byte OP_PUT = 1;
    private static final byte OP_DELETE = 2;

    private final File dataFile;
    private final File metaFile;
    private final RandomAccessFile raf;
    private final FileChannel channel;

    // tableId -> (rowId -> payload)
    private final Map<Integer, Map<Long, byte[]>> tables = new HashMap<>();

    public StorageEngine(File databaseFile) throws IOException {
        this.dataFile = databaseFile;
        this.metaFile = new File(databaseFile.getAbsolutePath() + ".meta");
        boolean exists = dataFile.exists();
        this.raf = new RandomAccessFile(dataFile, "rw");
        this.channel = raf.getChannel();
        if (!exists || channel.size() == 0) {
            initHeader();
        } else {
            loadAllRows();
        }
    }

    private void initHeader() throws IOException {
        ByteBuffer buf = ByteBuffer.allocate(HEADER_SIZE);
        buf.putInt(0x4D535145); // 魔数: MSQE (Mini SQlite Engine)
        buf.putInt(PAGE_SIZE);
        buf.putLong(HEADER_SIZE); // 预留字段
        while (buf.hasRemaining()) {
            buf.put((byte) 0);
        }
        buf.flip();
        channel.position(0);
        channel.write(buf);
        channel.force(true);
    }

    private void loadAllRows() throws IOException {
        long pos = HEADER_SIZE;
        long size = channel.size();
        ByteBuffer header = ByteBuffer.allocate(1 + 4 + 8 + 4);
        while (pos + header.capacity() <= size) {
            header.clear();
            channel.position(pos);
            int n = channel.read(header);
            if (n < header.capacity()) {
                break;
            }
            header.flip();
            byte op = header.get();
            int tableId = header.getInt();
            long rowId = header.getLong();
            int len = header.getInt();
            if (len < 0 || pos + header.capacity() + (op == OP_PUT ? len : 0) > size) {
                break;
            }
            pos += header.capacity();
            if (op == OP_PUT) {
                ByteBuffer payload = ByteBuffer.allocate(len);
                channel.read(payload);
                payload.flip();
                byte[] data = new byte[len];
                payload.get(data);
                tables.computeIfAbsent(tableId, id -> new HashMap<>()).put(rowId, data);
                pos += len;
            } else if (op == OP_DELETE) {
                Map<Long, byte[]> table = tables.get(tableId);
                if (table != null) {
                    table.remove(rowId);
                }
            } else {
                break;
            }
        }
    }

    public synchronized void putRow(int tableId, long rowId, byte[] payload) throws IOException {
        tables.computeIfAbsent(tableId, id -> new HashMap<>()).put(rowId, payload);
        appendRowToFile(tableId, rowId, payload);
    }

    public synchronized byte[] getRow(int tableId, long rowId) {
        Map<Long, byte[]> table = tables.get(tableId);
        return table == null ? null : table.get(rowId);
    }

    public synchronized List<RowRecord> scanTable(int tableId) {
        Map<Long, byte[]> table = tables.get(tableId);
        if (table == null || table.isEmpty()) {
            return List.of();
        }
        List<RowRecord> rows = new ArrayList<>(table.size());
        for (Map.Entry<Long, byte[]> entry : table.entrySet()) {
            rows.add(new RowRecord(tableId, entry.getKey(), entry.getValue()));
        }
        return rows;
    }

    public synchronized void deleteRow(int tableId, long rowId) throws IOException {
        Map<Long, byte[]> table = tables.get(tableId);
        if (table != null) {
            table.remove(rowId);
        }
        appendDeleteToFile(tableId, rowId);
    }

    public synchronized RowRecord getRowRecord(int tableId, long rowId) {
        Map<Long, byte[]> table = tables.get(tableId);
        if (table == null) {
            return null;
        }
        byte[] payload = table.get(rowId);
        return payload == null ? null : new RowRecord(tableId, rowId, payload);
    }

    private void appendRowToFile(int tableId, long rowId, byte[] payload) throws IOException {
        ByteBuffer header = ByteBuffer.allocate(1 + 4 + 8 + 4);
        header.put(OP_PUT);
        header.putInt(tableId);
        header.putLong(rowId);
        header.putInt(payload.length);
        header.flip();
        ByteBuffer body = ByteBuffer.wrap(payload);
        channel.position(channel.size());
        channel.write(header);
        channel.write(body);
        channel.force(true);
    }

    private void appendDeleteToFile(int tableId, long rowId) throws IOException {
        ByteBuffer header = ByteBuffer.allocate(1 + 4 + 8 + 4);
        header.put(OP_DELETE);
        header.putInt(tableId);
        header.putLong(rowId);
        header.putInt(0);
        header.flip();
        channel.position(channel.size());
        channel.write(header);
        channel.force(true);
    }

    public synchronized byte[] loadCatalog() throws IOException {
        if (!metaFile.exists()) {
            return null;
        }
        try (FileInputStream in = new FileInputStream(metaFile);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            in.transferTo(baos);
            return baos.toByteArray();
        }
    }

    public synchronized void saveCatalog(byte[] data) throws IOException {
        try (FileOutputStream out = new FileOutputStream(metaFile)) {
            out.write(data);
            out.getFD().sync();
        }
    }

    @Override
    public void close() throws IOException {
        channel.force(true);
        channel.close();
        raf.close();
    }

    public record RowRecord(int tableId, long rowId, byte[] payload) { }
}
